<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Binary vs String Transfer Benchmark</title>
    <script src="https://cdn.plot.ly/plotly-2.26.0.min.js"></script>
    <style>
      body {
        font-family: Tahoma, Serif;
        font-size: 10pt;
      }
      .info {
        font-size: 12pt;
      }
      .left {
        text-align: left;
      }
      .right {
        text-align: right;
      }
      .positive {
        color: green;
        font-weight: bold;
      }
      .negative {
        color: red;
        font-weight: bold;
      }
      .center {
        text-align: center;
      }
      table.resultTable {
        border: 1px solid black;
        border-collapse: collapse;
        empty-cells: show;
        width: 100%;
      }
      table.resultTable td {
        padding: 2px 4px;
        border: 1px solid black;
      }
      table.resultTable > thead > tr {
        font-weight: bold;
        background: lightblue;
      }
      table.resultTable > tbody > tr:nth-child(odd) {
        background: white;
      }
      table.resultTable > tbody > tr:nth-child(even) {
        background: lightgray;
      }
      .hide {
        display: none;
      }
    </style>
  </head>

  <body background-color="white">
    <h1>Binary vs String Transfer Benchmark</h1>

    <table>
      <tr>
        <td>
          <p class="info">
            This benchmark evaluates the message transfer speed between the
            renderer process and the browser process. <br />It compares the
            performance of binary and string message transfer.
          </p>
          <p class="info">
            <b>Note:</b> There is no progress indication of the tests because it
            significantly influences measurements. <br />It usually takes 30
            seconds (for 300 samples) to complete the tests.
          </p>
        </td>
      </tr>
      <tr>
        <td>
          Samples:
          <input
            id="sSamples"
            type="text"
            value="300"
            required
            pattern="[0-9]+"
          />
          <button id="sRun" autofocus onclick="runTestSuite()">Run</button>
        </td>
      </tr>
    </table>

    <div style="padding-top: 10px; padding-bottom: 10px">
      <table id="resultTable" class="resultTable">
        <thead>
          <tr>
            <td class="center" style="width: 8%">Message Size</td>
            <td class="center" style="width: 8%">
              String Round Trip Avg,&nbsp;ms
            </td>
            <td class="center" style="width: 8%">
              Binary Round Trip Avg,&nbsp;ms
            </td>
            <td class="center" style="width: 10%">Relative Trip Difference</td>
            <td class="center" style="width: 8%">String Speed,&nbsp;MB/s</td>
            <td class="center" style="width: 8%">Binary Speed,&nbsp;MB/s</td>
            <td class="center" style="width: 10%">Relative Speed Difference</td>
            <td class="center" style="width: 8%">String Standard Deviation</td>
            <td class="center" style="width: 8%">Binary Standard Deviation</td>
          </tr>
        </thead>
        <tbody>
          <!-- result rows here -->
        </tbody>
      </table>
    </div>

    <div id="round_trip_avg_chart">
      <!-- Average round trip linear chart will be drawn inside this DIV -->
    </div>

    <div id="box_plot_chart">
      <!-- Box plot of round trip time will be drawn inside this DIV -->
    </div>

    <script type="text/javascript">
      let tests = [];
      let box_plot_test_data = [];
      let round_trip_avg_plot_data = [];

      function nextTestSuite(testIndex) {
        const nextTestIndex = testIndex + 1;
        setTimeout(execTestSuite, 0, nextTestIndex);
      }

      function generateRandomString(size) {
        // Symbols that will be encoded as two bytes in UTF-8
        // so we compare transfer of the same amount of bytes
        const characters =
          "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя";
        let randomString = "";
        for (let i = 0; i < size; i++) {
          const randomIndex = Math.floor(Math.random() * characters.length);
          randomString += characters.charAt(randomIndex);
        }
        return randomString;
      }

      function generateRandomArrayBuffer(size) {
        const buffer = new ArrayBuffer(size);
        const uint8Array = new Uint8Array(buffer);
        for (let i = 0; i < uint8Array.length; i++) {
          uint8Array[i] = Math.floor(Math.random() * 256);
        }
        return buffer;
      }

      function reportError(errorCode, errorMessage) {
        console.error(`ErrorCode:${errorCode} Message:${errorMessage}`);
      }

      function sendString(size, testIndex) {
        const request = generateRandomString(size);
        const startTime = performance.now();
        const onSuccess = (response) => {
          const roundTrip = performance.now() - startTime;
          const test = tests[testIndex];
          test.totalRoundTrip += roundTrip;
          test.sample++;
          box_plot_test_data[testIndex].x.push(roundTrip);
          setTimeout(execTest, 0, testIndex);
        };

        window.cefQuery({
          request: request,
          onSuccess: onSuccess,
          onFailure: reportError,
        });
      }

      function sendArrayBuffer(size, testIndex) {
        const request = generateRandomArrayBuffer(size);
        const startTime = performance.now();
        const onSuccess = (response) => {
          const roundTrip = performance.now() - startTime;
          const test = tests[testIndex];
          test.totalRoundTrip += roundTrip;
          test.sample++;
          box_plot_test_data[testIndex].x.push(roundTrip);
          setTimeout(execTest, 0, testIndex);
        };

        window.cefQuery({
          request: request,
          onSuccess: onSuccess,
          onFailure: reportError,
        });
      }

      function getStandardDeviation(array, mean) {
        const n = array.length;
        if (n < 5) return null;
        return Math.sqrt(
          array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) /
            (n - 1)
        );
      }

      function execTest(testIndex) {
        const test = tests[testIndex];
        if (test.sample >= test.totalSamples) {
          return nextTestSuite(testIndex);
        }
        test.func(test.length, test.index);
      }

      function column(prepared, value) {
        return (
          "<td class='right'>" + (!prepared ? "-" : value.toFixed(3)) + "</td>"
        );
      }

      function relativeDiffColumn(prepared, value, isBiggerBetter) {
        if (!prepared) return "<td class='right'>-</td>";

        const isPositive = value >= 0 == isBiggerBetter;
        return [
          "<td class='right ",
          isPositive ? "positive" : "negative",
          "'>",
          value > 0 ? "+" : "",
          value.toFixed(2),
          "%</td>",
        ].join("");
      }

      function displayResult(test) {
        const id = "testResultRow_" + test.index;

        const markup = [
          "<tr id='",
          id,
          "'>",
          "<td class='left'>",
          test.name,
          "</td>",
          column(test.prepared, test.avgRoundTrip),
          column(test.prepared, test.avgRoundTripBin),
          relativeDiffColumn(test.prepared, test.relativeTripDiff, false),
          column(test.prepared, test.speed),
          column(test.prepared, test.speedBinary),
          relativeDiffColumn(test.prepared, test.relativeSpeedDiff, true),
          "<td class='right'>",
          !test.prepared || test.stdDeviation == null
            ? "-"
            : test.stdDeviation.toFixed(3),
          "</td>",
          "<td class='right'>",
          !test.prepared || test.stdDeviationBinary == null
            ? "-"
            : test.stdDeviationBinary.toFixed(3),
          "</td>",
          "</tr>",
        ].join("");

        const row = document.getElementById(id);
        if (row) {
          row.outerHTML = markup;
        } else {
          const tbody = document.getElementById("resultTable").tBodies[0];
          tbody.insertAdjacentHTML("beforeEnd", markup);
        }
      }
      function relativeDiff(left, right) {
        if (right != 0) {
          return ((left - right) / right) * 100;
        }
        return 0;
      }

      function buildTestResults(tests) {
        testResults = [];

        let oldRoundTrip = {
          x: [],
          y: [],
          type: "scatter",
          name: "String",
        };

        let newRoundTrip = {
          x: [],
          y: [],
          type: "scatter",
          name: "Binary",
        };

        for (let i = 0; i < tests.length / 2; i++) {
          const index = testResults.length;

          // Tests are in pairs - String and Binary
          const test = tests[i * 2];
          const testBin = tests[i * 2 + 1];

          const avgRoundTrip = test.totalRoundTrip / test.totalSamples;
          const avgRoundTripBin = testBin.totalRoundTrip / testBin.totalSamples;
          const relativeTripDiff = relativeDiff(avgRoundTripBin, avgRoundTrip);

          // In MB/s
          const speed = test.byteSize / (avgRoundTrip * 1000);
          const speedBinary = testBin.byteSize / (avgRoundTripBin * 1000);
          const relativeSpeedDiff = relativeDiff(speedBinary, speed);

          const stdDeviation = getStandardDeviation(
            box_plot_test_data[test.index].x,
            avgRoundTrip
          );
          const stdDeviationBinary = getStandardDeviation(
            box_plot_test_data[testBin.index].x,
            avgRoundTripBin
          );

          testResults.push({
            name: humanFileSize(test.byteSize),
            index: index,
            prepared: true,
            avgRoundTrip: avgRoundTrip,
            avgRoundTripBin: avgRoundTripBin,
            relativeTripDiff: relativeTripDiff,
            speed: speed,
            speedBinary: speedBinary,
            relativeSpeedDiff: relativeSpeedDiff,
            stdDeviation: stdDeviation,
            stdDeviationBinary: stdDeviationBinary,
          });

          oldRoundTrip.x.push(test.byteSize);
          newRoundTrip.x.push(test.byteSize);
          oldRoundTrip.y.push(avgRoundTrip);
          newRoundTrip.y.push(avgRoundTripBin);
        }

        round_trip_avg_plot_data = [oldRoundTrip, newRoundTrip];
        return testResults;
      }

      function buildEmptyTestResults(tests) {
        testResults = [];
        for (let i = 0; i < tests.length / 2; i++) {
          const index = testResults.length;
          const test = tests[i * 2];

          testResults.push({
            name: humanFileSize(test.byteSize),
            index: index,
            prepared: false,
          });
        }
        return testResults;
      }

      function resetTestsResults(totalSamples) {
        if (totalSamples <= 0) totalSamples = 1;

        // Reset tests results
        tests.forEach((test) => {
          test.sample = 0;
          test.totalRoundTrip = 0;
          test.totalSamples = totalSamples;
        });

        testResults = buildEmptyTestResults(tests);
        testResults.forEach((result) => displayResult(result));

        round_trip_avg_plot_data = [];
        box_plot_test_data.forEach((data) => {
          data.x = [];
        });
      }

      function queueTest(name, byteSize, length, testFunc) {
        const testIndex = tests.length;
        test = {
          name: name,
          byteSize: byteSize,
          length: length,
          index: testIndex,
          sample: 0,
          totalRoundTrip: 0,
          func: testFunc,
        };
        tests.push(test);

        box_plot_test_data.push({
          x: [],
          type: "box",
          boxpoints: "all",
          name: name,
          jitter: 0.3,
          pointpos: -1.8,
        });
      }

      function execTestSuite(testIndex) {
        if (testIndex < tests.length) {
          execTest(testIndex);
        } else {
          testsRunFinished();
        }
      }

      function startTests() {
        // Let the updated table render before starting the tests
        setTimeout(execTestSuite, 200, 0);
      }

      function execQueuedTests(totalSamples) {
        resetTestsResults(totalSamples);
        startTests();
      }

      function setSettingsState(disabled) {
        document.getElementById("sSamples").disabled = disabled;
        document.getElementById("sRun").disabled = disabled;
      }

      function testsRunFinished() {
        testResults = buildTestResults(tests);
        testResults.forEach((result) => displayResult(result));

        const round_trip_layout = {
          title: "Average round trip, μs (Smaller Better)",
        };
        Plotly.newPlot(
          "round_trip_avg_chart",
          round_trip_avg_plot_data,
          round_trip_layout
        );

        const box_plot_layout = {
          title: "Round Trip Time, μs",
        };
        Plotly.newPlot("box_plot_chart", box_plot_test_data, box_plot_layout);
        setSettingsState(false);
      }

      function humanFileSize(bytes) {
        const step = 1024;
        const originalBytes = bytes;

        if (Math.abs(bytes) < step) {
          return bytes + " B";
        }

        const units = [" KB", " MB", " GB"];
        let u = -1;
        let count = 0;

        do {
          bytes /= step;
          u += 1;
          count += 1;
        } while (Math.abs(bytes) >= step && u < units.length - 1);

        return bytes.toString() + units[u];
      }

      window.runTestSuite = () => {
        Plotly.purge("round_trip_avg_chart");
        Plotly.purge("box_plot_chart");
        setSettingsState(true);
        const totalSamples = parseInt(
          document.getElementById("sSamples").value
        );
        execQueuedTests(totalSamples);
      };

      const totalSamples = parseInt(document.getElementById("sSamples").value);
      queueTest("Empty String", 0, 0, sendString);
      queueTest("Empty Binary", 0, 0, sendArrayBuffer);
      for (let byteSize = 8; byteSize <= 512 * 1024; byteSize *= 2) {
        // Byte size of a string is twice its length because of UTF-16 encoding
        const stringLen = byteSize / 2;
        queueTest(
          humanFileSize(byteSize) + " String",
          byteSize,
          stringLen,
          sendString
        );
        queueTest(
          humanFileSize(byteSize) + " Binary",
          byteSize,
          byteSize,
          sendArrayBuffer
        );
      }
      resetTestsResults(totalSamples);
    </script>
  </body>
</html>
